package ss.pku.jsa.type.inference;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Vector;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IOpenable;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IParent;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;

import ss.pku.jsa.type.constraint.solver.Native;
import ss.pku.jsa.type.preference.JqualPreferencePage;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class JqualPlugin extends AbstractUIPlugin {
	private static JqualPlugin plugin;
	private ResourceBundle resourceBundle;
	public static boolean inferReadonly;
	public static Analysis analysis = null;
	public static final String QUALIFIER_PROP = "QUALIFIER";
	public static final String javadocLog = "javadocLog";
	public static final String qualifierLog = "qualifierLog";
	public static final String objectLog = "objectLog";
	public static final String worklistLog = "worklistLog";
	public static final String missingClassLog = "missingClassLog";
	public static final String constraintLog = "constraintLog";
	public static final String warningLog = "warningLog";
	static boolean runTest = false;
	public static boolean useClosure = false;
	public static boolean useJavalib = false;
	public static boolean useJavalibPartial = false;

	public static boolean debug = false;
	public static boolean profile = false;
	public static boolean fieldSensitive = false;
	public static boolean obeyFSAnnotations = false;
	public static boolean banshee = false;
	public static boolean useJavariSelfLoopsHack = false;

	public static boolean contextSensitive = false;
	public static boolean allowMerge = false;

	public static boolean inferOpaque = false;
	public static boolean inferInterned = true;
	public static boolean useLongNames = false;
	public static boolean useFFISubtypes;
	public static String ffiAnnotationsPath = null;
	public static boolean ffiInferPublic = false;
	public static boolean ffiInferEnum = false;
	public static boolean ffiInferTrans = false;
	public static int ffiFirstPtr = 0;
	public static int ffiNumPtrs = 10;
	public static boolean logInferred;
	public static boolean limitFields = true;

	public static boolean showContraintMsgs = false;
	public static boolean alwaysMergeObjects = false;
	public static boolean logConstraints = false;
	public static String latticePath;
	public static String ffiQualsPath;
	public static IJavaProject currentFirstProject = null;
	public static IWorkspaceRoot root = getWorkspace().getRoot();
	public static String rootPath = root.getLocation().toString();
	public static boolean noGates = false;

	private static Map<String, PrintStream> debugFiles = new HashMap();
	public static boolean showEmptyQuals;
	public static boolean runFromCommandLine;
	// public static MessageConsole jqualConsole = findConsole("JQUAL");
	public static PrintStream output = new PrintStream(System.out);

	public static PrintStream out = System.out;
	public static String workspaceRootDir = ResourcesPlugin.getWorkspace()
			.getRoot().getRawLocation().toOSString();

	static void dumpConfig() {
		printlnLog("configLog", "useClosure" + useClosure + "\n" + "useJavalib"
				+ useJavalib + "\n" + "useJavalibPartial" + useJavalibPartial
				+ "\n" + "debug" + debug + "\n" + "profile" + profile + "\n"
				+ "fieldSensitive" + fieldSensitive + "\n"
				+ "obeyFSAnnotations" + obeyFSAnnotations + "\n" + "banshee"
				+ banshee + "\n" + "javariGlobalHack" + useJavariSelfLoopsHack
				+ "\n" + "contextSensitive" + contextSensitive + "\n"
				+ "allowMerge" + allowMerge + "\n" + "inferOpaque"
				+ inferOpaque + "\n" + "inferInterned" + inferInterned + "\n"
				+ "useLongNames" + useLongNames + "\n" + "useFFISubtypes"
				+ useFFISubtypes + "\n" + "ffiAnnotationsPath"
				+ ffiAnnotationsPath + "\n" + "ffiInferPublic" + ffiInferPublic
				+ "\n" + "ffiInferEnum" + ffiInferEnum + "\n" + "ffiInferTrans"
				+ ffiInferTrans + "\n" + "ffiFirstPtr" + ffiFirstPtr + "\n"
				+ "ffiNumPtrs" + ffiNumPtrs + "\n" + "logInferred"
				+ logInferred + "\n" + "limitFields" + limitFields + "\n"
				+ "showContraintMsgs" + showContraintMsgs + "\n"
				+ "alwaysMergeObjects" + alwaysMergeObjects + "\n"
				+ "logConstraints" + logConstraints + "\n" + "latticePath"
				+ latticePath + "\n" + "ffiQualsPath" + ffiQualsPath + "\n"
				+ "noGates" + noGates + "\n" + "currentFirstProject"
				+ currentFirstProject + "\n");
	}

	public JqualPlugin() {
		plugin = this;
	}

	public static JqualPlugin getDefault() {
		return plugin;
	}

	public static void debug(Object o) {
		String s = o.toString();
		if (debug)
			out.println(s);
	}

	public static void profile(String s) {
		if (profile)
			out.println(s + " " + Profile.timestamp());
	}

	public static IWorkspace getWorkspace() {
		return ResourcesPlugin.getWorkspace();
	}

	public static String getResourceString(String key) {
		ResourceBundle bundle = getDefault().getResourceBundle();
		try {
			return bundle.getString(key);
		} catch (MissingResourceException localMissingResourceException) {
		}
		return key;
	}

	public ResourceBundle getResourceBundle() {
		return this.resourceBundle;
	}

	public void start(BundleContext context) throws Exception {
		super.start(context);

		plugin = this;
		try {
			this.resourceBundle = ResourceBundle
					.getBundle("jqual.JqualResources");
		} catch (MissingResourceException localMissingResourceException) {
			this.resourceBundle = null;
		}
		try {
			getStateLocation();
		} catch (Exception ise) {
			System.err.println("Error: illegal state exception raised");
			ise.printStackTrace();
		}
	}

	public void stop(BundleContext context) throws Exception {
		super.stop(context);
	}

	public static void prepareForAnalysis() {
		Profile.logMemStart("prepareForAnalysis/new Analysis()");
		analysis = new Analysis();
		Profile.logMemEnd();
		clearLogs();
		// jqualConsole.activate();
		boolean polyFlag = (contextSensitive) || (fieldSensitive)
				|| (obeyFSAnnotations);
		if (latticePath == null) {
			throw new IllegalArgumentException("Lattice path cannot be null");
		}
		if (banshee)
			Native.initCQual("", "", false, false, 0, 0);
		else {
			CQual.resetCqual(latticePath, inferOpaque ? ffiQualsPath : "",
					polyFlag, useFFISubtypes, ffiInferEnum, ffiFirstPtr,
					ffiNumPtrs);
		}

		analysis.maxEnum = Native.getMaxEnum();

		analysis.initQuals();
	}

	public static void readPreferences() {
		IPreferenceStore prefs = JqualPreferencePage.prefs;
		contextSensitive = prefs.getBoolean("contextSensitive");
		useClosure = prefs.getBoolean("typeClosurePreference");
		useLongNames = prefs.getBoolean("useLongQualifierNamesPreference");
		logInferred = prefs.getBoolean("logInferredTypes");
		fieldSensitive = prefs.getBoolean("fieldSensitive");
		obeyFSAnnotations = prefs.getBoolean("obeyfieldSensitiveMarkups");
		allowMerge = prefs.getBoolean("allowMerge");
		useClosure = prefs.getBoolean("typeClosurePreference");
		useJavalib = prefs.getBoolean("includeJavaLibraries");

		inferReadonly = prefs.getBoolean("javariAnalysis");
		inferInterned = prefs.getBoolean("inferInterned");
		showEmptyQuals = JqualPreferencePage.prefs
				.getBoolean("showEmptyQualifiersPreference");
		profile = prefs.getBoolean("profilePreference");
		debug = prefs.getBoolean("debugPreference");
		logConstraints = prefs.getBoolean("logConstraints");

		latticePath = prefs.getString("latticePreference");
		out.println("LATTICE: " + latticePath);
		if (latticePath == null) {
			latticePath = "";
		}
		inferOpaque = prefs.getBoolean("doFFIAnalysis");
		if (inferOpaque) {
			ffiAnnotationsPath = prefs
					.getString("annotationsFileForFFIAnalysis");
			if ((ffiAnnotationsPath == null)
					|| (ffiAnnotationsPath.length() == 0)) {
				if (currentFirstProject != null)
					ffiAnnotationsPath = rootPath
							+ currentFirstProject.getPath().toString() + "/"
							+ "annotations.txt";
				else
					throw new IllegalArgumentException(
							"To perform FFI analysis you must specify a qualifier file.");
			}
			ffiQualsPath = prefs.getString("qualsFileForFFIAnalysis");
			if ((ffiQualsPath == null) || (ffiQualsPath.length() == 0)) {
				if (currentFirstProject != null)
					ffiQualsPath = rootPath
							+ currentFirstProject.getPath().toString() + "/"
							+ "qual_list.txt";
				else
					throw new IllegalArgumentException(
							"To perform FFI analysis you must specify an FFI annotations file.");
			}
			useFFISubtypes = prefs.getBoolean("allowSubtypesInFFIAnalysis");

			ffiInferEnum = prefs.getBoolean("ffiInferEnums");
			ffiInferPublic = prefs.getBoolean("ffiPublic");
			ffiInferTrans = prefs.getBoolean("ffiTransparent");
			ffiFirstPtr = prefs.getInt("ffiFirstPtrQuals");
			ffiNumPtrs = prefs.getInt("ffiNumPtrQuals");
		}
	}

	public static void listPreferences() {
		out.println(prefListString());
	}

	public static String prefListString() {
		StringBuilder ret = new StringBuilder();
		ret.append("-------------- Settings: ----------------");
		ret.append("\n");
		ret.append("Context Sensitive: " + contextSensitive);
		ret.append("\n");
		ret.append("Field Sensitive: " + fieldSensitive);
		ret.append("\n");
		ret.append("  Merging Fields: " + allowMerge);
		ret.append("\n");
		ret.append("  Limiting Fields from supertype: " + limitFields);
		ret.append("\n");

		ret.append("-------------- Analyses: ----------------");
		ret.append("\n");
		ret.append("Javari analysis: " + inferReadonly);
		ret.append("\n");
		ret.append("Interned analysis: " + inferInterned);
		ret.append("\n");
		ret.append("FFI analysis: " + inferOpaque);
		ret.append("\n");
		if (inferOpaque) {
			ret.append("  FFI subtypes: " + useFFISubtypes);
			ret.append("\n");
			ret.append("  FFI Quals: " + ffiQualsPath);
			ret.append("\n");
			ret.append("  FFI Annotations: " + ffiAnnotationsPath);
			ret.append("\n");
			ret.append("  FFI Enum?: " + (ffiInferEnum ? "true" : "false"));
			ret.append("\n");
			ret.append("  FFI Public?: " + (ffiInferPublic ? "true" : "false"));
			ret.append("\n");
			ret.append("  FFI Transparent?: "
					+ (ffiInferTrans ? "true" : "false"));
			ret.append("\n");
			ret.append("  FFI ptr start: " + ffiFirstPtr);
			ret.append("\n");
			ret.append("  FFI ptr count: " + ffiNumPtrs);
			ret.append("\n");
		}
		ret.append("-----------------------------------------\n");
		ret.append("\n");
		return ret.toString();
	}

	public static String ffiRunString() {
		return ffiFirstPtr + "_" + (ffiNumPtrs + ffiFirstPtr)
				+ (ffiInferPublic ? "_pub" : "")
				+ (ffiInferEnum ? "_enum" : "")
				+ (ffiInferTrans ? "_trans" : "");
	}

	public static Analysis analyze(IOpenable element) {
		return analyze(new IOpenable[] { element }, true);
	}

	@SuppressWarnings("unused")
	public static Analysis analyze(IOpenable[] elements, boolean readPrefs) {
		currentFirstProject = null;
		for (IOpenable element : elements) {
			if ((element instanceof IJavaProject)) {
				currentFirstProject = (IJavaProject) element;
				break;
			}

		}

		readPreferences();

		Profile.init();
		profile("Start Analysis");
		Profile.logStart("*Start Analysis");
		Profile.logStart("prepareForAnalysis()");
		prepareForAnalysis();
		Profile.logEnd();
		dumpConfig();

		Profile.logStart("*gather compilation units");
		ICompilationUnit[] compilationUnits = getCompilationUnits(elements);
		for (int i = 0; i < compilationUnits.length; i++) {
			printlnLog("fileListLog", compilationUnits[i].getElementName());
			analysis.originalSet.add(compilationUnits[i]);
		}

		Profile.logEnd();

		StringBuffer results = new StringBuffer();
		if (compilationUnits != null) {
			try {
				ASTParser parser = ASTParser.newParser(3);

				if (useClosure) {
					Profile.logStart("*Type Closure");

					for (int k = 0; k < compilationUnits.length; k++) {
						analysis.addToVisitList(compilationUnits[k]);
						IJavaProject project = compilationUnits[k]
								.getJavaProject();
						analysis.projects.add(project);
					}

					if (useJavalib) {
						IJavaProject project = getJavaProject("javalib");
						if ((project == null) || (!project.exists()))
							out.println("No 'javalib' project available. Libraries will not be included.");
						else
							analysis.projects.add(project);
					}
					if (useJavalibPartial) {
						IJavaProject project = getJavaProject("javalibPartial");
						analysis.projects.add(project);
					}
					if (inferReadonly) {
						IJavaProject project = getJavaProject("readonlyLibraryStubs");
						analysis.projects.add(project);
					}
					Vector closure = new Vector();
					while (analysis.typesLeftToVisit()) {
						ICompilationUnit c = analysis.nextTypeToVisit();
						closure.add(c);

						parser.setSource(c);
						parser.setResolveBindings(true);

						CompilationUnit compilationUnit = null;
						if (c.getElementName().equals("TdsData.java"))
							out.println("Parse " + c.getElementName());
						try {
							compilationUnit = (CompilationUnit) parser
									.createAST(null);
						} catch (Exception e) {
							throw new RuntimeException(
									"Exception encountered during JDT parse of "
											+ getName(c) + ":\n" + e);
						}
						String fileName = c.getUnderlyingResource().getName();
						TypeClosureVisitor visitor = new TypeClosureVisitor(
								analysis, compilationUnit, fileName);
						compilationUnit.accept(visitor);
					}
					closure.size();

					compilationUnits = (ICompilationUnit[]) closure
							.toArray(compilationUnits);

					Profile.logEnd();
				}

				if (runTest) {
					for (int k = 0; k < compilationUnits.length; k++) {
						parser.setSource(compilationUnits[k]);
						parser.setResolveBindings(true);
						CompilationUnit compilationUnit = (CompilationUnit) parser
								.createAST(null);

						String fileName = compilationUnits[k]
								.getUnderlyingResource().getName();
						TestVisitor visitor = new TestVisitor(compilationUnit,
								fileName, analysis);
						compilationUnit.accept(visitor);
					}

				} else {
					Profile.logStart("*Visitors");

					if ((!fieldSensitive) && (obeyFSAnnotations)) {
						for (int k = 0; k < compilationUnits.length; k++) {
							parser.setSource(compilationUnits[k]);
							parser.setResolveBindings(true);

							Profile.logStart("*FS Visitor: Create AST");
							CompilationUnit compilationUnit = (CompilationUnit) parser
									.createAST(null);
							Profile.logEnd();

							String filename = compilationUnits[k]
									.getUnderlyingResource().getName();

							Profile.logStart("*Visitors: Find FS Annotations");
							compilationUnit.accept(new FieldSensitiveVisitor(
									compilationUnit, filename, analysis));
							Profile.logEnd();
						}

					}

					for (int k = 0; k < compilationUnits.length; k++) {
						parser.setSource(compilationUnits[k]);
						parser.setResolveBindings(true);

						Profile.logStart("*Visitors: Create AST");
						CompilationUnit compilationUnit = (CompilationUnit) parser
								.createAST(null);
						Profile.logEnd();

						String filename = compilationUnits[k]
								.getUnderlyingResource().getName();
						debug("  " + filename);

						Profile.logStart("*Visitors: Declaration Visitor");
						DeclarationVisitor visitor = new DeclarationVisitor(
								compilationUnit, filename, analysis,
								analysis.originalSet
										.contains(compilationUnits[k]));
						compilationUnit.accept(visitor);
						Profile.logEnd();

						ConstraintVisitor cvisitor = new ConstraintVisitor(
								compilationUnit, filename, analysis,
								analysis.originalSet
										.contains(compilationUnits[k]));
						Profile.logStart("*Visitors: Constraint Visitor");
						compilationUnit.accept(cvisitor);
						Profile.logEnd();
					}

					analysis.makeConservativeAssumptionsForMissingTypes();

					if (inferOpaque) {
						printlnLog("constraintLog",
								"------------FFI-----------");
						Profile.logStart("*Add FFI Qualifiers");
						new FFIResults(ffiAnnotationsPath);
						Profile.logEnd();

						printlnLog("constraintLog",
								"------------END FFI-----------");
					}

					Profile.logEnd();
				}
			} catch (Exception jme) {
				jme.printStackTrace();
				System.err.println(jme);
				throw new IllegalArgumentException();
			}

			Profile.logStart("*Propogate Constraints");
			analysis.propagateConstraints();
			Profile.logEnd();

			byte[] buffer = new byte[1024];

			Profile.logStart("*Finish CQual");
			FileInputStream fis = CQual.finish();
			Profile.logEnd();

			Profile.logStart("*Report Errors");

			String message = "";
			int bytesRead;
			if (fis != null)
				try {
					while ((bytesRead = fis.read(buffer)) != -1) {
						message = message.concat(new String(buffer, 0,
								bytesRead));
					}
				} catch (IOException e) {
					e.printStackTrace();
					throw new Error(
							"IO error reading results from cqual library.");
				}
			else {
				message = "FinishCqual returned NULL\n";
			}

			message = "Qualifier constraint errors were detected:\n" + message;

			results.append("\n======================\n" + message
					+ "\n====================\n");
			results.append("Analyzed " + analysis.originalSet.size()
					+ " original files\n");

			analysis.results = results.toString();

			Profile.logEnd();
			Profile.logEnd();

			if (debug) {
				printlnLog("objectLog", analysis.dumpObjects());
			}
			profile("End Analysis");

			if (inferReadonly) {
				analysis.getJavariResults();
			}

			if (logInferred) {
				printlnLog("inferredResultsLog",
						analysis.declarationsToStringWithQuals());
			}
			return analysis;
		}
		return null;
	}

	public static ICompilationUnit[] getCompilationUnits(IOpenable[] elements) {
		if (elements != null) {
			ArrayList compilationUnits = new ArrayList();
			for (int i = 0; i < elements.length; i++) {
				compilationUnits.addAll((Collection) Arrays
						.asList(getCompilationUnits(elements[i])));
			}
			ICompilationUnit[] compAr = new ICompilationUnit[compilationUnits
					.size()];
			compilationUnits.toArray(compAr);
			return compAr;
		}
		return null;
	}

	public static ICompilationUnit[] getCompilationUnits(IOpenable element) {
		if (element != null) {
			try {
				if (((element instanceof IPackageFragmentRoot))
						&& (((IPackageFragmentRoot) element).getElementName()
								.contains(".jar"))) {
					out.println("Ignoring jar file: "
							+ ((IPackageFragmentRoot) element).getElementName());
					return new ICompilationUnit[0];
				}

				if ((element instanceof ICompilationUnit)) {
					ICompilationUnit c = (ICompilationUnit) element;
					ICompilationUnit[] ret = { c };
					return ret;
				}

				IJavaElement[] children = ((IParent) element).getChildren();

				if (children != null) {
					ArrayList compUnits = new ArrayList();
					for (int j = 0; j < children.length; j++) {
						if ((children[j] instanceof IOpenable)) {
							compUnits
									.addAll((Collection) Arrays
											.asList(getCompilationUnits((IOpenable) children[j])));
						}

					}

					ICompilationUnit[] ret = new ICompilationUnit[compUnits
							.size()];
					compUnits.toArray(ret);
					return ret;
				}
			} catch (Exception e) {
				e.printStackTrace();
				throw new Error();
			}
		}
		return null;
	}

	public static IJavaProject getJavaProject(String name) {
		IWorkspaceRoot myWorkspaceRoot = getWorkspace().getRoot();
		IProject project = null;
		try {
			project = myWorkspaceRoot.getProject(name);
		} catch (Exception e) {
			out.println("Could not open project '" + name + "':\n" + e);
			return null;
		}
		if (project == null)
			return null;
		if (!project.exists())
			return null;
		return JavaCore.create(project);
	}

	public static String getName(ICompilationUnit c) {
		if (c == null)
			out.println("null sent");
		else if (c.findPrimaryType() == null) {
			out.println("in " + c);
		}
		return c.findPrimaryType().getFullyQualifiedName();
	}

	public static void printlnLog(String filename, String s) {
		printLog(rootPath + "/" + filename, s + "\n");
	}

	public static void printLog(String filename, String s) {
		PrintStream out = (PrintStream) debugFiles.get(filename);
		if (out == null) {
			try {
				out = new PrintStream(new FileOutputStream(filename), false);
			} catch (FileNotFoundException localFileNotFoundException) {
				System.err.println("Could not open " + filename
						+ ". Using System.out instead.");
			}
			debugFiles.put(filename, out);
		}
		out.print(s);
	}

	public static void clearLogs() {
		for (PrintStream p : debugFiles.values())
			p.close();
		debugFiles.clear();
	}

	public static boolean isStatic(IVariableBinding v) {
		return (v.getModifiers() & 0x8) != 0;
	}

	// private static MessageConsole findConsole(String name) {
	// ConsolePlugin plugin = ConsolePlugin.getDefault();
	// IConsoleManager conMan = plugin.getConsoleManager();
	// IConsole[] existing = conMan.getConsoles();
	// for (int i = 0; i < existing.length; i++) {
	// if (name.equals(existing[i].getName()))
	// return (MessageConsole) existing[i];
	// }
	// MessageConsole myConsole = new MessageConsole(name, null);
	// conMan.addConsoles(new IConsole[] { myConsole });
	// return myConsole;
	// }

	public static void outputInEclipseConsole(boolean setting) {
		if (setting)
			out = new PrintStream(output);
		else
			out = System.out;
	}
}
